/*
 * Copyright 2009 Lukasz Wozniak
 * Licensed under the Apache License, Version 2.0 (the "License"); 
 * you may not use this file except in compliance with the License. 
 * You may obtain a copy of the License at 
 *
 * http://www.apache.org/licenses/LICENSE-2.0 
 *
 * Unless required by applicable law or agreed to in writing, software 
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and 
 * limitations under the License.
 */
package pl.lwozniak.http;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.ProtocolException;
import java.net.Proxy;
import java.net.URI;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import pl.lwozniak.blip.message.BlipMessagePart;

/**
 * Simple HTTP conector based on HttpURLConnection.
 * 
 * @author Lukasz Wozniak
 * 
 */
public class SimpleJavaHttpConnector implements HttpConnector {

	private static final HttpHeader[] EMPTY_HEADERS = new HttpHeader[] {};

	private static final int BUFFER_SIZE = 50;;

	private Proxy proxy;

	private HashMap<URI, URL> urls = new HashMap<URI, URL>();

	private HttpHeader[] defaultHeaders;

	private HttpBodyGenerator bodyGenerator = new HttpBodyGenerator();

	private final Pattern p = Pattern.compile(".*filename\\=\"([\\w\\.]*)\".*");

	SimpleJavaHttpConnector(Proxy proxy) {
		super();
		this.proxy = proxy;
	}

	private URL getURL(URI target) throws HttpRequestException {
		if (urls.containsKey(target)) {
			return urls.get(target);
		} else {
			URL url;
			try {
				url = target.toURL();
				if (url.getProtocol().equalsIgnoreCase("http")) {
					urls.put(target, url);
					return url;
				} else {
					throw new HttpRequestException("Unknown protocol in URL "
							+ target);
				}
			} catch (MalformedURLException e) {
				throw new HttpRequestException("Bad URL", e);
			}
		}
	}

	private void putHeaders(HttpURLConnection con, HttpHeader[] headers) {
		for (int i = 0; i < headers.length; i++) {
			if (headers[i].isSingle()) {
				con.setRequestProperty(headers[i].getName(), headers[i]
						.getValue());
			} else {
				con.addRequestProperty(headers[i].getName(), headers[i]
						.getValue());
			}
		}
	}

	private HttpURLConnection createConnection(URI target)
			throws HttpRequestException {
		URL url = getURL(target);
		try {

			HttpURLConnection con = null;
			if (proxy == null) {
				con = (HttpURLConnection) url.openConnection();
			} else {
				con = (HttpURLConnection) url.openConnection(proxy);
			}
			if (defaultHeaders != null && defaultHeaders.length > 0) {
				putHeaders(con, defaultHeaders);
			}
			return con;
		} catch (IOException e) {
			throw new HttpRequestException("Error creating connection.", e);
		}
	}

	/* (non-Javadoc)
	 * @see pl.lwozniak.http.HttpConnector#doDownload(pl.lwozniak.http.HttpHeader[], java.net.URI)
	 */
	public Download doDownload(HttpHeader[] headers, URI target)
			throws HttpRequestException {
		HttpURLConnection con = createConnection(target);
		if (headers != null) {
			putHeaders(con, headers);
		}
		try {
			con.setRequestMethod("GET");
			if (!con.getDoInput()) {
				con.setDoInput(true);
			}
			if (con.getResponseCode() == 200) {
				String contentType = con
						.getHeaderField(HttpStrings.CONTENT_TYPE);
				String fileName = extractFilename(contentType);

				InputStream in = con.getInputStream();
				ConnectionHelper ch = new SimpleConnectionHelper(con);

				return new Download(ch, contentType, fileName, in);
			} else {
				throw new HttpRequestException(con.getResponseCode(), con
						.getResponseMessage());
			}
		} catch (ProtocolException e) {
			// right... just in case
			throw new RuntimeException(e);
		} catch (IOException e) {
			throw new HttpRequestException(e);
		}

	}

	/**
	 * Provide filename if it was supplied by the server.<br/>
	 * If not then its users problem to recognize content (he maybe knew what
	 * was downloaded) i name his files accordingly.
	 * 
	 * @param contentType
	 *            content type header from server
	 * @return filename or null if not found
	 */
	private String extractFilename(String contentType) {
		if (contentType != null) {
			Matcher m = p.matcher(contentType);
			if (m.matches()) {
				return m.group(1);
			}
		}
		return null;
	}

	private HttpResult doRequest(String method, HttpHeader[] headers,
			URI target, BlipMessagePart body) throws HttpRequestException {
		HttpURLConnection con = createConnection(target);
		if (headers != null) {
			putHeaders(con, headers);
		}

		try {
			con.setRequestMethod(method);
		} catch (ProtocolException e) {
			throw new HttpRequestException("Error creating " + method
					+ " connection", e);
		}

		try {
			con.setUseCaches(false);
			if (body != null) {
				bodyGenerator.writeBody(con, body);
			}
			if (!con.getDoInput()) {
				con.setDoInput(true);
			}

			return handleResult(con);
		} catch (IOException e) {
			throw new HttpRequestException("Error executing request", e);
		}

	}

	/**
	 * Handles request result.
	 * 
	 * @param con
	 * @return
	 * @throws IOException
	 */
	private HttpResult handleResult(HttpURLConnection con) throws IOException {
		HttpResult res = new HttpResult();
		res.setCode(con.getResponseCode());
		res.setMessage(con.getResponseMessage());
		res.setHeaders(extractHeaders(con.getHeaderFields()));
		if (con.getResponseCode() == 200 || con.getResponseCode() == 201) {
			res.setBody(readInputStream(con.getInputStream(), con
					.getContentEncoding(), con.getContentLength()));
		} else {
			// do something on HTTP error?
			if (con.getErrorStream() != null) {
				res.setBody(readInputStream(con.getErrorStream(), con
						.getContentEncoding(), con.getContentLength()));
			}
		}
		con.disconnect();
		return res;
	}

	/**
	 * Reads stream into String since blip returns only texts.
	 * 
	 * @param in
	 * @param encoding
	 * @param length
	 * @return
	 * @throws IOException
	 */
	private String readInputStream(InputStream in, String encoding, int length)
			throws IOException {
		ByteArrayOutputStream data = null;
		if (length > 0) {
			data = new ByteArrayOutputStream(length);
		} else {
			data = new ByteArrayOutputStream(2 * BUFFER_SIZE);
		}

		byte[] buffer = new byte[BUFFER_SIZE];
		int cnt = in.read(buffer);
		while (cnt > 0) {
			data.write(buffer, 0, cnt);
			cnt = in.read(buffer);
		}
		in.close();
		data.close();

		// is this really necessary? should be UTF-8 anyway...
		if (encoding == null) {
			encoding = DEFAULT_ENCODING;
		}
		return new String(data.toByteArray(), encoding);
	}

	private HttpHeader[] extractHeaders(Map<String, List<String>> headerFields) {
		if (headerFields != null && !headerFields.isEmpty()) {
			ArrayList<HttpHeader> headers = new ArrayList<HttpHeader>(
					headerFields.size());
			Iterator<String> iterator = headerFields.keySet().iterator();
			while (iterator.hasNext()) {
				String key = iterator.next();
				List<String> val = headerFields.get(key);
				if (!val.isEmpty()) {
					if (val.size() == 1) {
						HttpHeader h = new HttpHeader();
						h.setName(key);
						h.setValue(val.get(0));
						headers.add(h);
					} else {
						Iterator<String> vals = val.iterator();
						while (vals.hasNext()) {
							HttpHeader h = new HttpHeader();
							h.setName(key);
							h.setValue(vals.next());
							h.setSingle(false);
							headers.add(h);
						}
					}
				}
			}
			return headers.toArray(new HttpHeader[headers.size()]);
		} else {
			return EMPTY_HEADERS;
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * pl.lwozniak.http.HttpConnector#doDelete(pl.lwozniak.http.HttpHeader[],
	 * java.net.URI, java.lang.String)
	 */
	public HttpResult doDelete(HttpHeader[] headers, URI target,
			BlipMessagePart body) throws HttpRequestException {
		return doRequest("DELETE", headers, target, body);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see pl.lwozniak.http.HttpConnector#doGet(pl.lwozniak.http.HttpHeader[],
	 * java.net.URI, java.lang.String)
	 */
	public HttpResult doGet(HttpHeader[] headers, URI target,
			BlipMessagePart body) throws HttpRequestException {
		return doRequest("GET", headers, target, body);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see pl.lwozniak.http.HttpConnector#doPost(pl.lwozniak.http.HttpHeader[],
	 * java.net.URI, java.lang.String)
	 */
	public HttpResult doPost(HttpHeader[] headers, URI target,
			BlipMessagePart body) throws HttpRequestException {
		return doRequest("POST", headers, target, body);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see pl.lwozniak.http.HttpConnector#doPut(pl.lwozniak.http.HttpHeader[],
	 * java.net.URI, java.lang.String)
	 */
	public HttpResult doPut(HttpHeader[] headers, URI target,
			BlipMessagePart body) throws HttpRequestException {
		return doRequest("PUT", headers, target, body);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * pl.lwozniak.http.HttpConnector#setDefaultHeaders(pl.lwozniak.http.HttpHeader
	 * [])
	 */
	public void setDefaultHeaders(HttpHeader[] headers) {
		this.defaultHeaders = headers;
	}

	private class SimpleConnectionHelper implements ConnectionHelper {
		private HttpURLConnection con;

		public SimpleConnectionHelper(HttpURLConnection con) {
			super();
			this.con = con;
		}

		public void close() throws IOException {
			con.disconnect();
		}

	}

}
